package biz.datalk.industrialland.common.util;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * Redis 分布式锁
 *
 * @author tarofang@163.com
 * @date 2019-10-25
 */

@SuppressWarnings({"unused", "WeakerAccess", "UnusedReturnValue"})
@Component
@Slf4j
public class RedisLock {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /** 由于是单例，所以用于标识客户端 */
    private ThreadLocal<String> lockFlag = new ThreadLocal<String>();

    /**
     * 释放锁脚本，原子操作
     * <pre>
     *     if redis.call("get",KEYS[1]) == ARGV[1]
     *     then
     *         return redis.call("del",KEYS[1])
     *     else
     *         return 0
     *     end
     * </pre>
     *
     * @see <a href="https://redis.io/commands/set">参考地址</a>
     */
    private static final String UNLOCK_LUA = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1]) else return 0 end";

    private static final Charset UTF8 = StandardCharsets.UTF_8;

    /**
     * 获取锁，原子操作
     *
     * @param lockKey  锁的key
     * @param lockVal  锁的值，建议使用UUID
     * @param expire   锁的过期时间
     * @param timeUnit 过期时间单位
     *
     * @return true 获取到锁 否则为获取到锁
     *
     * @author tarofang@163.com
     * @date 2019-10-25
     */
    public boolean tryLock(final String lockKey, final String lockVal, final long expire, final TimeUnit timeUnit) {
        try {
            RedisCallback<Boolean> callback = (connection) -> {
                String tmpVal = StringUtils.isEmpty(lockVal) ? UUID.randomUUID().toString() : lockVal.trim();
                lockFlag.set(tmpVal);
                Expiration seconds = Expiration.seconds(timeUnit.toSeconds(expire));
                return connection.set(lockKey.getBytes(UTF8), tmpVal.getBytes(UTF8), seconds, RedisStringCommands.SetOption.SET_IF_ABSENT);
            };

            Boolean execute = redisTemplate.execute(callback);
            return execute == null ? Boolean.FALSE : execute;
        } catch (Exception ex) {
            log.error("Redis lock error.", ex);
        }
        return Boolean.FALSE;
    } ///~

    /**
     * 获取锁，原子操作
     *
     * @param lockKey      锁的key
     * @param lockVal      锁的值，建议使用UUID
     * @param expireSecond 锁的过期时间, 单位：秒
     *
     * @return true - 获取到锁 否则为获取到锁
     *
     * @author tarofang@163.com
     * @date 2019-10-25
     * @see #tryLock(String, String, long, TimeUnit)
     */
    public boolean tryLock(final String lockKey, final String lockVal, final long expireSecond) {
        return tryLock(lockKey, lockVal, expireSecond, TimeUnit.SECONDS);
    } ///~

    /**
     * 释放锁
     *
     * @param lockKey 锁的key
     * @param lockVal 锁的值，建议使用UUID
     *
     * @return true - 释放锁成功，否则释放失败
     *
     * @author tarofang@163.com
     * @date 2019-10-25
     */
    public boolean releaseLock(final String lockKey, final String lockVal) {
        // 释放锁的时候，有可能因为持锁之后方法执行时间大于锁的有效期，此时有可能已经被另外一个线程持有锁，所以不能直接删除
        try {
            RedisCallback<Boolean> callback = connection -> {
                String tmpVal = StringUtils.isEmpty(lockKey) ? lockFlag.get() : lockVal;
                return connection.eval(UNLOCK_LUA.getBytes(UTF8), ReturnType.BOOLEAN, 1, lockKey.getBytes(UTF8), tmpVal.getBytes(UTF8));
            };
            Boolean execute = redisTemplate.execute(callback);
            return execute == null ? Boolean.FALSE : execute;
        } catch (Exception ex) {
            log.error("release lock occured an exception", ex);
        } finally {
            lockFlag.remove();
        }
        return Boolean.FALSE;
    } ///~

    /**
     * 获取 redis 锁的 value 值
     *
     * @param lockKey 锁的key
     *
     * @return null 为获取到value值，否则获取到value值
     *
     * @author tarofang@163.com
     * @date 2019-10-25
     */
    public String get(final String lockKey) {
        try {
            RedisCallback<String> callback = (connection) -> {
                byte[] bytes = connection.get(lockKey.getBytes(UTF8));
                return bytes == null ? null : new String(bytes, UTF8);
            };
            return redisTemplate.execute(callback);
        } catch (Exception ex) {
            log.error("Get redis occurred an exception", ex);
        }
        return null;
    }

    // public boolean lockWait(final String lockKey, final String lockVal, final long expire, final TimeUnit timeUnit) {
    //     boolean isLocked = false;
    //     while (true) {
    //         isLocked = tryLock(lockKey, lockVal, expire, timeUnit);
    //         if (isLocked) {
    //             break;
    //         }
    //     }
    //     return isLocked;
    // }
}
